package com.ncr.travel.tas.configuration.management;

import java.lang.reflect.Method;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.ncr.travel.tas.configuration.model.ConfigurationNode;
import com.ncr.travel.tas.configuration.model.ConfigurationValue;
import com.ncr.travel.tas.configuration.service.messaging.ConfigurationNodeCriteria;
import com.ncr.travel.tas.configuration.service.messaging.ConfigurationService;
import com.ncr.travel.tas.shared.configuration.ConfigurationInjector;
import com.ncr.travel.tas.shared.configuration.ConfigurationNodeEnum;
import com.ncr.travel.tas.shared.configuration.annotations.Configuration;
import com.ncr.travel.tas.shared.configuration.annotations.ConfigurationSet;

/**
 * This utility class will synchronize and parse configuration annotations on classes.
 * The configurations are persisted by the configuration service.
 * @author eb250081
 *
 */
@Component
public class ConfigurationManagerImpl implements ConfigurationManager {

	private ConfigurationService configurationService;

	@Autowired
	public ConfigurationManagerImpl(ConfigurationService configurationService) {
		super();
		this.configurationService = configurationService;
	}

	/**
	 * returns either the name of the @ConfigurationSet annotation OR
	 * the class's canonical name if it is not marked up with @ConfigurationSet
	 * @param object
	 */
	private <T> String getConfigurationSetName(T object) {

		// check to see if the object has an @ConfigurationSet annotation
		ConfigurationSet setAnnotation = object.getClass().getAnnotation(ConfigurationSet.class);

		// default to the object's canonical name if no @ConfigurationSet
		// annotation
		if (null == setAnnotation) {
			return object.getClass().getCanonicalName();
		}

		return setAnnotation.name();
	}

	/**
	 * returns either the name of the @ConfigurationSet annotation OR
	 * the class's canonical name if it is not marked up with @ConfigurationSet
	 * @param object
	 */
	private <T> ConfigurationNodeEnum getConfigurationSetType(T object) {

		// check to see if the object has an @ConfigurationSet annotation
		ConfigurationSet setAnnotation = object.getClass().getAnnotation(ConfigurationSet.class);

		// default to FRAMEWORK type if not specified
		if (null == setAnnotation) {
			return ConfigurationNodeEnum.FRAMEWORK;
		}

		return setAnnotation.type();
	}

	@Override
	public <T> void setValues(T object) throws ConfigurationManagerException {

		ConfigurationInjector injector = new ConfigurationInjector();

		// let's ask the injector to return a list of all the @Configuration
		// annotations
		List<String> configurationNames = injector.getConfigurationNames(object);

		// if the class is not marked up at all, return and do nothing
		if (configurationNames.size() == 0) {
			return;
		}

		this.setValues(object, configurationNames);
	}

	/* (non-Javadoc)
	 * @see com.ncr.travel.tas.configuration.management.ConfigurationManager#synchronize(T)
	 */
	@Override
	public <T> void setValues(T object, List<String> configurationNames) throws ConfigurationManagerException {

		try {

			// this utility class manages reflection to find @Configuration
			// annotations
			ConfigurationInjector injector = new ConfigurationInjector();

			// Step 1: Get the @ConfigurationSet name on the object. If it is
			// not
			// explicitly marked up, then it will default to the class's full
			// canonical name
			String setName = this.getConfigurationSetName(object);
			ConfigurationNodeEnum setType = this.getConfigurationSetType(object);

			// find the configuration set, if it exists.
			List<ConfigurationNode> found = null;

			ConfigurationNodeCriteria criteria = new ConfigurationNodeCriteria(setType, setName);

			found = configurationService.findConfiguration(criteria);

			// create a base ConfigurationNode\ConfigurationValues if one is not
			// present.
			// by default, leave the object alone and return.
			if (null != found && found.size() == 0) {
				createDefaultConfiguration(object, injector, setName, setType, configurationNames);
				return;
			}

			// we absolutely should not have more than one. If so, the database
			// constraints are wrong AND
			// code somewhere is inserting duplicates instead of updating.
			if (found.size() > 1) {
				throw new ConfigurationManagerException("More than one configuration was found for object " + criteria);
			}

			// we found a matching @ConfigurationSet. Set all values on the
			// object.
			this.setConfiguration(object, found.get(0));

		} catch (Exception e) {

			throw new ConfigurationManagerException(e);
		}

	}

	private <T> void createDefaultConfiguration(T object, ConfigurationInjector injector, String setName, ConfigurationNodeEnum setType, List<String> configurationNames) throws IllegalAccessException,
			ConfigurationManagerException {

		// if it was not found, we need to create a root container for all
		// values
		ConfigurationNode node = new ConfigurationNode(setType);
		node.setName(setName);
		node.setDescription("Default configuration set");
		node.setUpdatedBy(this.getClass().getCanonicalName());
		node.setUpdatedOn(new Date());

		Set<ConfigurationValue> children = new HashSet<ConfigurationValue>();

		// for all the configuration names that are found on the object
		// get the value of the field associated with the @Configuration
		for (String c : configurationNames) {

			// look up the @Configuration by this name
			Configuration target = injector.getConfiguration(object, c);

			ConfigurationValue cv;
			cv = new ConfigurationValue(target.type().name());
			cv.setKeyName(target.key());
			cv.setTag(target.tag());

			Object value;
			value = injector.getValueByConfiguration(object, c);

			cv.setValue(value);

			// add the value to the parent configuration
			children.add(cv);

		}

		// attach the children to the node
		node.setConfigurations(children);

		// save the configuration
		try {
			configurationService.addConfiguration(node);
		} catch (Exception e) {
			throw new ConfigurationManagerException(e);
		}
	}

	/**
	 * Based on the configuration values, set the object's value
	 * @param <T>
	 * @param object
	 * @param found
	 * @throws Exception
	 */
	private <T> void setConfiguration(T object, ConfigurationNode found) throws ConfigurationManagerException {

		ConfigurationInjector injector = new ConfigurationInjector();

		// ensure that the right configuration set is being applied
		String annotation = this.getConfigurationSetName(object);

		if (!annotation.equals(found.getName()) && !annotation.equals(object.getClass().getCanonicalName())) {

			throw new ConfigurationManagerException("internal error. @ConfigurationSet " + annotation == null ? object.getClass().getCanonicalName() : annotation + " not found in ConfigurationNode " + found.getName());
		}

		// cycle through the configuration values and set the object's matching
		// values
		for (ConfigurationValue v : found.getConfigurations()) {
			injector.setConfiguration(object, v.getKeyName(), v.getValue());
		}
	}

	/* (non-Javadoc)
	 * @see com.ncr.travel.tas.configuration.management.ConfigurationManager#isAnnotatedForConfiguration(java.util.Map.Entry)
	 */
	@Override
	public boolean isAnnotatedForConfiguration(Entry<String, GetterSetterPair> item) {

		GetterSetterPair pair = item.getValue();
		if (pair.isBalanced() == false) {
			return false;
		}

		return (item.getValue().getSetter().isAnnotationPresent(Configuration.class) || item.getValue().getGetter().isAnnotationPresent(Configuration.class));

	}

	/* (non-Javadoc)
	 * @see com.ncr.travel.tas.configuration.management.ConfigurationManager#extractGetterSetterName(java.lang.reflect.Method)
	 */
	@Override
	public String extractGetterSetterName(Method getterSetter) throws ConfigurationManagerException {

		if (!isGetter(getterSetter) && !isSetter(getterSetter)) {
			throw new ConfigurationManagerException("Method is not a getXXXX or setXXXX format");
		}
		String name;
		name = getterSetter.getName().substring(3);
		return name;
	}

	private boolean isGetter(Method method) {

		if (!method.getName().startsWith("get")) {
			return false;
		}

		if (method.getParameterTypes().length != 0) {
			return false;
		}

		if (void.class.equals(method.getReturnType())) {
			return false;
		}

		return true;
	}

	private boolean isSetter(Method method) {

		if (!method.getName().startsWith("set")) {
			return false;
		}

		if (method.getParameterTypes().length != 1) {
			return false;
		}

		return true;
	}

}
